#!usr/bin/env python
# -*- coding:utf-8 _*-
"""
@author:zhengxin
@file: 012_Pandas_Advanced_Features.py
@time: 2025/2/18  10:52
# @describe: Pandas 高级功能
"""

"""
一、数据合并与连接
    Pandas 提供了多个方法来合并和连接不同的 DataFrame，例如 merge()、concat() 和 join()。这些方法常用于处理多个数据集和复杂的合并任务。
    
    1. merge() — 数据库风格的连接
        merge() 方法允许根据某些列对两个 DataFrame 进行合并，类似 SQL 中的 JOIN 操作。支持内连接、外连接、左连接和右连接。

参数	            说明
left	    左侧 DataFrame
right	    右侧 DataFrame
how	        合并方式，支持 'inner', 'outer', 'left', 'right'
on	        连接的列名（如果两侧列名不同，可使用 left_on 和 right_on）
left_on	    左侧 DataFrame 的连接列
right_on	右侧 DataFrame 的连接列
suffixes	添加后缀，以区分重复的列名
     
"""
import pandas as pd

left = pd.DataFrame({'ID': [1, 2, 3], 'Name': ['Alice', 'Bob', 'Charlie']})
right = pd.DataFrame({'ID': [1, 2, 4], 'Age': [24, 27, 22]})

# 使用 merge 进行内连接
result = pd.merge(left, right, on="ID", how="inner")
print(result)

""" 
2. concat() — 沿轴连接
    concat() 用于将多个 DataFrame 沿指定轴（行或列）进行连接，常用于行合并（垂直连接）或列合并（水平连接）。
    
    参数	                说明
    objs	        需要合并的 DataFrame 列表
    axis	        合并的轴，0 表示按行合并，1 表示按列合并
    ignore_index	是否忽略索引，重新生成索引（默认为 False）
    keys	        为合并的对象提供层次化索引
 """
import pandas as pd

df1 = pd.DataFrame({"A": [1, 2, 3]})
df2 = pd.DataFrame({"A": [4, 5, 6]})

# 行合并
result = pd.concat([df1, df2], axis=0, ignore_index=True)
print(result)
print()

""" 3. join() — 基于索引连接
    join() 方法是 Pandas 中的简化连接操作，通常用于基于索引将多个 DataFrame 连接。

参数	说明
other	需要连接的另一个 DataFrame
how	合并方式，支持 'left', 'right', 'outer', 'inner'
on	使用的连接列，默认基于索引 
 
"""
import pandas as pd

left = pd.DataFrame({"A": [1, 2, 3]}, index=[1, 2, 3])
right = pd.DataFrame({"B": [4, 5, 6]}, index=[1, 2, 4])

# 使用join 进行连接
result = left.join(right, how="inner")
print(result)
print()

""" 二、透视表与交叉表
    Pandas 提供了 pivot_table() 方法来创建透视表，和 crosstab() 方法来计算交叉表。透视表和交叉表都非常适合数据的汇总和重新排列。

    1. pivot_table() — 创建透视表
    参数	            说明
    data	    输入的数据
    values	    要汇总的列
    index	    用作行索引的列
    columns	    用作列索引的列
    aggfunc	    聚合函数，默认为 mean，可以是 sum, count 等
    fill_value	填充缺失值

"""
import pandas as pd

data = {'Date': ['2024-01-01', '2024-01-02', '2024-01-03', '2024-01-04'],
        'Category': ['A', 'B', 'A', 'B'],
        'Sales': [100, 150, 200, 250]}
df = pd.DataFrame(data)

# 创建透视表
pivot_table = pd.pivot_table(df, values="Sales", index="Date", columns="Category", aggfunc="sum", fill_value=0)
print(pivot_table)
print()

""" 2. crosstab() — 创建交叉表
        参数	          说明
        index	    行标签
        columns 	列标签
        values	    用于计算的数据（可选）
        aggfunc	    聚合函数，默认 count
 
 """
import pandas as pd

data = {'Category': ['A', 'B', 'A', 'B', 'A', 'B'],
        'Region': ['North', 'South', 'North', 'South', 'West', 'East']}
df = pd.DataFrame(data)

# 创建交叉表
cross_table = pd.crosstab(df["Category"], df["Region"])
print(cross_table)

""" 三、自定义函数应用
Pandas 提供了多种方法应用自定义函数，用于数据清洗和转换。

1. apply() — 应用函数到 DataFrame 或 Series 上
apply() 方法允许在 DataFrame 或 Series 上应用自定义函数，支持对行或列进行操作。

参数	                说明
func	        需要应用的函数
axis	        默认为 0，表示按列应用；1 表示按行应用
raw	            是否传递原始数据（默认为 False）
result_type	    定义输出的类型，如 expand, reduce, broadcast

"""
import pandas as pd

df = pd.DataFrame({"A": [1, 2, 3, 4], "B": [10, 20, 30, 40]})


# 定义自定义函数
def custom_func(x):
    return x * 2


# 在列上应用函数
df["A"] = df["A"].apply(custom_func)
print(df)
print()

"""
2. applymap() — 在整个 DataFrame 上应用函数
        applymap() 只能应用于 DataFrame，作用于 DataFrame 中的每个元素。
        
DataFrame.applymap 方法已经被弃用，建议使用 DataFrame.map 方法替代

参数	        说明
func	需要应用的函数
"""
import pandas as pd

df = pd.DataFrame({"A": [1, 2, 3], "B": [4, 5, 6]})
# 在 DataFrame 上应用自定义函数
df = df.map(lambda x: x ** 2)
print(df)
print()



""" 
3. map() — 应用函数到 Series 上
        map() 可以对 Series 中的每个元素应用一个函数或一个映射关系。

参数	            说明
arg	    应用的函数，字典或 Series

"""
import pandas as pd

df = pd.DataFrame({'A': ['cat', 'dog', 'rabbit']})

# 使用字典进行映射
df["A"] = df["A"].map({"cat": "kitten", "dog": "puppy"})
print(df)
print()



"""
四、分组操作与聚合
    Pandas 中的 groupby() 方法非常强大，可以用于分组聚合、转换数据和过滤数据。
    通过 groupby()，可以将数据根据某些条件分组，进行聚合运算，如求和、均值、计数等。

1. groupby() — 数据分组
参数	         说明
by	    按照哪个列或索引分组
axis	分组的轴，默认为 0，即按行进行分组
level	按照索引的级别进行分组（适用于 MultiIndex）
"""
import pandas as pd

df = pd.DataFrame({
    'Category': ['A', 'B', 'A', 'B', 'A', 'B'],
    'Value': [10, 20, 30, 40, 50, 60]
})
# 按照 Category 列进行分组，并计算每组的总和
grouped = df.groupby("Category")["Value"].sum()
print(grouped)
print()


""" 2. 聚合操作（agg()）
agg() 用于执行复杂的聚合操作，可以传入多个函数来同时计算多个聚合值。

参数	        说明
func	聚合函数，可以是字符串或自定义函数
 """
df = pd.DataFrame({
    'Category': ['A', 'B', 'A', 'B', 'A', 'B'],
    'Value': [10, 20, 30, 40, 50, 60]
})

# 使用 agg() 来进行多个聚合操作
grouped = df.groupby("Category")["Value"].agg(["sum", "min", "max"])
print(grouped)
print()



"""
五、时间序列处理
    Pandas 提供了强大的时间序列处理功能，包括日期解析、频率转换、日期范围生成、时间窗口操作等。

1. date_range() — 生成时间序列
参数	        说明
start	    起始日期
end	        结束日期
periods	    生成的时间点数
freq	    频率（如 D 表示天，H 表示小时等）
"""
import pandas as pd

# 生成时间序列
date_range = pd.date_range(start="2025-02-18", periods=5, freq="D")
print(date_range)
print()


""" 
2. 日期和时间的偏移
    使用 pd.Timedelta() 可以进行时间的加减操作。
"""
# 日期偏移
date = pd.to_datetime("2025-02-18")
new_date = date + pd.Timedelta(days=10)
print(new_date)


"""
3. 时间窗口操作（Rolling, Expanding）
    使用 rolling() 和 expanding() 方法进行滚动和扩展窗口操作，常用于时间序列中的移动平均等计算。

方法          	    说明
rolling()	    计算滚动窗口操作，常用于移动平均等
expanding()	    计算扩展窗口操作，累计值
"""
df = pd.DataFrame({'Value': [10, 20, 30, 40, 50]})
# 计算 3天滚动平均
df["Rolling_Mean"] = df["Value"].rolling(window=3).mean()
print(df)
print()



""" 
六、缺失值处理
Pandas 提供了多种方法来处理缺失值（如 NaN）。常见的操作包括填充缺失值、删除缺失值等。

方法	            说明
isna()	    检查缺失值，返回布尔值
fillna()	填充缺失值
dropna()	删除包含缺失值的行或列

"""
import pandas as pd
import numpy as np


df = pd.DataFrame({
    "A": [1, 2, np.nan, 4],
    "B": [5, np.nan, 7, 8],
})
# 填充缺失值
df_filled = df.fillna(0)
print(df_filled)
print()



"""
七、多重索引（MultiIndex）
    Pandas 提供了多重索引（MultiIndex）的功能，使得可以在 DataFrame 或 Series 中处理复杂的数据结构，尤其适用于层次化的数据。
    通过多重索引，我们能够对数据进行分组、选择、切片以及聚合等操作。

1. 创建多重索引
可以通过 pd.MultiIndex.from_tuples()、pd.MultiIndex.from_product() 或者 set_index() 方法来创建多重索引。

方法 1: pd.MultiIndex.from_tuples()
使用元组来创建多重索引，每个元组对应一个索引层级。

参数	        说明
tuples	每个元组对应一个索引值
names	每个索引级别的名称（可选）

"""
# 创建元组
index_tuples = [("A", 1), ("A", 2), ("B", 1), ("B", 2)]
# 创建多重索引
multi_index = pd.MultiIndex.from_tuples(index_tuples, names=["letter", "Number"])
# 创建 DataFrame
df = pd.DataFrame({"Value": [10, 20, 30, 40]}, index=multi_index)
print(df)



"""
方法 2: pd.MultiIndex.from_product()
使用多个列表的笛卡尔积来创建多重索引，适合用于数据维度较多的情况。

参数	            说明
iterables	多个列表或数组
names	    每个索引级别的名称（可选）
"""
# 创建多个列表
index_values = [["A", "B"], [1, 2]]
# 创建多重索引
multi_index = pd.MultiIndex.from_product(index_values, names=["Letter", "Number"])

# 创建 DataFrame
df = pd.DataFrame({"Value": [10, 20, 30, 40]}, index=multi_index)
print(df)
print()



"""
方法 3: 使用 set_index() 创建多重索引
    set_index() 方法可以将 DataFrame 的列转换为多重索引，适用于从已有的数据创建多重索引。

参数      	说明
keys	用作索引的列名（可以是单列或多列）
"""
data = {
    'Letter': ['A', 'A', 'B', 'B'],
    'Number': [1, 2, 1, 2],
    'Value': [10, 20, 30, 40]
}
df = pd.DataFrame(data)
# 设置多重索引
df.set_index(["Letter", "Number"], inplace=True)
print(df)



"""
2. 多重索引的操作
    1. 访问多重索引数据
    可以通过层级索引来访问数据。通过 loc[] 或 xs()（cross-section）可以方便地进行数据选择。
    
    1.1 使用 loc[] 选择数据:
    
    1.2 使用 xs() 获取交叉数据:
            xs() 方法可以在多重索引中选择指定级别的切片。
"""
data = {
    'Letter': ['A', 'A', 'B', 'B'],
    'Number': [1, 2, 1, 2],
    'Value': [10, 20, 30, 40]
}

df = pd.DataFrame(data)
# 设置多重索引
df.set_index(["Letter", "Number"], inplace=True)
# 选择“A”类型，所有“Number” 为1 的数据
print(df.loc["A", 1])

# 使用 xs 获取 'Number' 为 1 的所有数据
print(df.xs(1, level="Number"))



"""
3. 排序多重索引
    Pandas 的 sort_index() 方法支持对多重索引进行排序。
"""
# 按照多重索引排序
df_sorted = df.sort_index(level=["Letter", "Number"], ascending=[True, False])
print(df_sorted)
print()



"""
4. 聚合操作
    多重索引结合 groupby() 可以进行强大的聚合操作，适用于复杂数据的统计分析。
"""
# 使用 groupby 对多重索引进行聚合
df_grouped = df.groupby(["Letter", "Number"]).sum()
print(df_grouped)


"""
5. 重设索引
    可以使用 reset_index() 方法将多重索引重置为普通的列。
"""
# 重设索引
df_reset = df.reset_index()
print(df_reset)


"""
6. 多重索引的缺失值
    多重索引中的缺失值可以通过 fillna() 或 dropna() 来处理，类似于普通索引。
"""
# 示例数据中引入缺失值
data = {
    'Letter': ['A', 'A', 'B', 'B'],
    'Number': [1, 2, 1, 2],
    'Value': [10, None, 30, 40]
}

df = pd.DataFrame(data)
df.set_index(["Letter", "Number"], inplace=True)

# 填充缺失值
df_filled = df.fillna(0)
print(df_filled)